import java.util.Queue;
import java.util.LinkedList;


public class CPU implements Runnable {
	
	/**
	 * The instruction address where the cpu is currently at 
	 * for a process. (program counter)
	 */
	private int my_PC;  
	
	/** List of the Process control blocks */
	private PCBList my_PCB_List;
	
	/** Current Process Control BLock of the running process*/
	private Pcb my_PCB;
	
	/** Current running process */
	private Process my_process;
	
	 /** Reference Shared Memory. */
    private Memory my_memory;

    /** Reference to Scheduler.*/
    private Scheduler my_scheduler;
    
    /** boolean for if the cpu is running
     */
    private boolean is_running;
    
    /**timer interrupt*/
    private static boolean interrupted_by_timer;
    
    /** Queuue for io*/
    private Queue<Pcb> IOinterruptsToHandle;
    
    /** Disk */
    private IODevice disk;
    
    /** keyboard */
    private IODevice keyboard;
    
    /** pid */
    private int new_pid;

	
	public CPU(final PCBList the_PCB_List, final Scheduler the_scheduler, final Memory the_memory) throws InterruptedException {
		
		IOinterruptsToHandle = new LinkedList<Pcb>();
		
		my_PC = 0;
		my_memory = the_memory;
		my_scheduler = the_scheduler;
		my_PCB_List = the_PCB_List;
		is_running = true;
		
		my_PCB = null;
		my_process = null;
	}
	public void run() {
		
		while(is_running) {
			my_PCB = my_scheduler.getNextPCB();  //scheduler sends next PCB		
			my_process = my_PCB.getProcess();
			my_scheduler.resetTimer(); // reset the process cpu time
			new_pid = 0; // reset the flag
			my_PC = my_PCB.getPC();
			
			my_PCB.setState(0);  //set current process to running
			
			System.out.println("Scheduler: Running process "+ my_PCB.getID() + 
		            " ("+ my_process.toString() +") next.");
			
			System.out.println(my_PCB_List.toString());
			
			if(!IOinterruptsToHandle.isEmpty())
			{
				IOInterruptHandler();
			}
			
			while(my_PC < my_process.getNoSteps()) {
				
				if(!IOinterruptsToHandle.isEmpty())
				{
					IOInterruptHandler();
				}
				
				// new_pid = my_scheduler.usePolicy();
				
				if(my_process.getReqList().contains(my_PC)) {					
					serviceHandler();
					
					if(my_PCB.getState() == 2) {  //if blocked
						break;
					}
				} else if(interrupted_by_timer && new_pid != -1) { // any interruption by scheduler
					if(my_scheduler.getPolicy() == 0) {  //timer effects only in round robin
						my_PCB.setState(1);	//set it to ready
						my_PCB.setPC(my_PC); // save PC
						interrupted_by_timer = false;
						break;	
					} else if(my_scheduler.getPolicy() == 1 && interrupted_by_timer) {  //lottery finishes process 
						my_PCB.setState(1);	//set it to ready
						my_PCB.setPC(my_PC); // save PC
						my_PCB.setPC(0);
						interrupted_by_timer = false;
						break;
					} else if(my_scheduler.getPolicy() == 2 && interrupted_by_timer) { //priority
						my_PCB.setState(1);	//set it to ready
						my_PCB.setPC(my_PC); // save PC
						my_PCB.setPC(0);
						interrupted_by_timer = false;
						break;
						
					}	
				}
				my_PC++;
				my_PC = my_PC % my_process.getNoSteps() - 1;
			}
			my_PCB.setPC(my_PC);
			
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		
	}
	public void endRun() {
		is_running = false;
	}
	private void serviceHandler() {		
		if (my_process.getProcType() == 0) {   //is computation process
			disk.addPcb(my_PCB);  //make call to disk
			blockPCB();
            
		} else if (my_process.getProcType() == 1) {   //is io process
			disk.addPcb(my_PCB);  //make call to disk
			blockPCB();
			
		} else if(my_process.getProcType() == 2) {  //is keyboard process
			keyboard.addPcb(my_PCB);  //make call to disk
			blockPCB();
			
		} else if(my_process.getProcType() == 3) {  //is producer process
			putDataInMemory(); 
			 
		} else if(my_process.getProcType() == 4) {  //is consumer process
			getDataFromMemory(); 
			
		}
		
	}
	
	private void blockPCB() {
		my_PCB.setPC(my_PC);
        my_PCB.setState(2); //block current process
		
        System.out.println("Process " + my_PCB.getID()+ " BLOCKED");
	}
	
	private void putDataInMemory() {
		final int mutex_position = my_PCB.getMutex();
		
		//if memory at the position is full
		//returns true or false
		if(!my_memory.FreeAtPosition(mutex_position)) {
			blockPCB();
		
		} else {  //memory at spot is empty
			my_memory.PutData(mutex_position);  //puts 1 at the mutex position
			//System.out.println("Data put in location " + mutex_position);
			 
			//find the consumer of this mutex
			final Pcb pcb = my_PCB_List.getConFromMutex(mutex_position); 
			pcb.setState(1);  //set it to ready
			
            
		}
	}
	
	private void getDataFromMemory() {
		final int mutex_position = my_PCB.getMutex();
		
		if(my_memory.FreeAtPosition(mutex_position)) {  //is empty
			blockPCB();
			
		} else {  //has data
			
			my_PCB.setState(0);  //set it to running
			
			my_memory.DeleteData(mutex_position);  //resets data at index
			
			final Pcb pcb = my_PCB_List.getProFromMutex(mutex_position); 
			pcb.setState(1); //set state to ready
			
		}

	}
	
	private void IOInterruptHandler()
	{
		// store the current state
		Pcb backup_Pcb = my_PCB;
		my_PCB.setState(1);
		
		my_PCB = IOinterruptsToHandle.poll();  // remove and return the object
		my_process = my_PCB.getProcess();
		my_PC = my_PCB.getPC();
		
		serviceHandler();
		
		// restore the old State
		my_PCB = backup_Pcb;
		my_PCB.setState(0);
		my_process = backup_Pcb.getProcess();
		my_PC = backup_Pcb.getPC();
	}
	
	private void send_IOInterruptSignal(final Pcb the_pcb)
	{
		IOinterruptsToHandle.add(the_pcb); // cpu will be notified of an interrupt
	}
	
	/**
	 * An interrupt is sent by an io device
	 * @param the_pcb the_pcb that can be set to the ready state
	 */
	 protected void IOInterrupt(final Pcb the_pcb) {
		 the_pcb.setState(1); //set it to ready
		 send_IOInterruptSignal(the_pcb); // send signal to cpu
	 }

	 public static void setTimerInterrupt() {
		 interrupted_by_timer = true;
	 }
	 
	 public static void resetTimerInterrupt() {
		 interrupted_by_timer = false;
	 }
	 
	 protected void makeIODevice(final IODevice device) {
		 if(device.getDeviceType() == IODevice.DeviceType .KEYBOARD) {
			 keyboard = device;
		 } else {
			 disk = device;
		 }
	 }
	 public void setNew_pid(int pid)
	 {
		 new_pid = pid;
	 }
}
